独家|前端高性能队列应用实践探秘
导语
bull可以让node快速实现异步调用、流量削峰、分布式定时任务,进一步打破前端在高并发、分布式方面的限制。
本篇内容涵盖前端遇到的一些复杂的应用场景及实践经验,希望能给大家提供一些不一样的思路。
背景
2019年的今天,nodejs已经成为了前端研发必备的技术,几乎所有的前端团队都会涉及nodejs开发,nodejs在前端的应用场景,主要包括以下几个:
命令行工具:各种支持前端业务团队快速开发的脚手架,功能一般包括创建项目、编译项目、启动调试项目等。
中间层:为了解决跨域而出现的接口代理服务,以及为提升页面展示性能而出现的页面渲染服务等。
和前端相关平台的后端服务:比如配置平台,api管理平台等等。
这些场景都是在打造前端基础设施,使其更加完善,但是有时候前端也会遇到一些相对复杂的场景,比如面向用户或处理数据的服务,需要实现轻量级的异步调用、流量削峰和分布式定时任务,这时会使用到队列框架。
后端有非常成熟的消息队列框架,比如kafka、rocketmq等,但是这些队列中间件对于前端来说并不算友好,因为首先他们是Java开发的,在Java体系中使用,是前端人员并不熟悉的技术栈,学习成本较高,对接公司服务的成本也相对较高,需要开发node客户端。其次,还是过于重量级了,比如rocketmq一般是独立集群部署,占用服务器资源非常多。
所以,前端如果想使用队列,就必须挑选基于node开发的、更适合的轻量级框架。比较常见的包括Kue、Bull、Bee、Agenda。下面的表格展示了四个框架的特点,可以清晰的看出四者的区别:
Kue是TJ大神的早期作品,最早流行起来的queue框架,但是更新不太频繁。Bull 是目前功能最完善的框架,同时支持Jobs和Messages。Bee在开发构成中参考了Bull,更加专注于小粒度任务的处理,并极大的优化了这种场景的性能,同时也只提供相对小的功能集。以上三种框架均基于redis,而Agenda是基于mongo的。
经过权衡,我们在线上报错收集场景中选用了Bull,基于以下几点原因:
线上报错是发生时间短、请求峰值高的场景,所以Rate Limiter是必须的; Bull 有强大的分布式Jobs处理能力,使前端的定时任务开发变的更加高效和合理; Bull 基于redis,目前58内部Redis的应用变得更为主流,mongo逐渐被其他服务替代,所以我们也会顺应公司的趋势选择基于redis的框架;
Bull 的更新相对频繁,有比较好的项目交流,同时很多web框架都有Bull的中间件,而且Bull有多个可视化项目更加方便管理和查看。
分布式定时任务
前端的定时任务通常会通过在linux服务器中,通过设置crontab来实现,但是存在一个问题,crontab是在集群的每一台服务器分别部署,是单机工具,自身缺乏分布式和集中管理的能力。所以为了简单,一般会在集群中挑选一个节点做某一个定时任务的部署,这种定时任务比较少,任务耗时比较少的时候还可以接受,当定时任务很多就会出现资源利用不合理、调配不及时的现象。
阿里的node框架egg也实现了定时任务功能,同样不支持分布式。但是给了两种解决方案,一种是类似于上面说的,通过配置写死服务器ip和定时任务的对应关系,不过如果是docker部署就不合适了,docker的ip会变,另一种是扩展自定义定时任务类型,把自己node进程当做被调度者,让其他分布式工具来调度管理,这是会让项目变的更加复杂。
经过调研,发现Bull本身就是基于redis分布式框架,又有强大的job管理能力,包含重复定时、沙箱处理等功能,可以完美的解决现存问题。把定时任务,作为job加入bull,设置启动时间,设置是否自定义进程,通过bull的调度分布式集群的进程去完成工作。
核心原理
那么Bull是如何实现分布式调度和按指定顺序高性能执行的呢?答案是Bull的底层工具Redis和Redis的brpoplpush命令,原理图如下:
redis的list类型天生支持用作消息队列,list类型是使用双向列表实现,支持从头部和尾部插入新的元素,并且效率非常高,即使list中已经存储了百万级的元素,也可以在常量时间插入完成。
消费者是通过绑定在queue的执行函数消费数据,在绑定执行函数时会向redis发起一个brpoplpush命令,这个命令将list中的最后一个元素返回给node进程。当list为nil,brpoplpush会阻塞连接,直到等待超时,或有另一个node进程的生产者添加job数据。当node进程处理完一个job,会再次发起brpoplpush。
一个集群包含多个节点服务器,部署着多个node进程,这些node进程会共同持有一个queue,每个node进程都可以生产数据和消费数据,意味着每个node进程都可以添加job,都会向redis发起brpoplpush命令,去争夺下一个需要被执行的job,redis执行哪个进程的brpoplpush命令,哪个进程就获得job,正是这种特性打造了一个非常简单又高效的分布式任务调度系统。
实践经验
在实践中,还需要注意几个问题:
及时清理job。queue中的job执行完务必要清理,否则会导致两个问题:
1)redis的keys会逐渐增加,最终导致内存被占满。
2)redis服务器和node进程之间会频繁通信,交换list数据,node集群的节点服务器和redis服务器的网卡流量会非常大,而node因为会缓存job数据,内存也会逐渐增大,很容易超过node进程设置的max-old-space-size,最终不断重启。
redis与部署环境一一对应。通常公司会有多个部署环境,一般是生产环境、沙箱环境、测试环境,前端也经常会有本地环境。在非生产环境的部署中,尽量部署与研发环境分别对应的、不同的redis,否则会出现几个环境的node进程争夺job的问题,影响测试效率。 构造可以JSON序列化的job。job构建的时候传入的是一个对象,请让这个对象可以被序列化成JSON字符串,不要增加方法,或者持有process、ctx等对象,因为要在redis中存储。
总结和规划
本文介绍的是在前端错误收集的场景下,如何利用Bull这个强大的node队列框架解决流量高峰,定时任务的分布式调度的问题。前端作为可以全栈开发的技术方向,在落地过程中总会遇到各技术方向都会遇到的问题,如何在前端体系下去实现,是摆在所有前端人前面的疑问,以上就是我们在摸索过程中积累的一些经验,如果有什么遗漏或者错误,欢迎大家指正,也欢迎大家一起相互交流继续探索。
参考文献
END
留言区分享你的评价、感想或实践经验,截止11月14日14:00点获赞数第一名的留言即可获得50元京东购物卡一张或技术类图书一本~
点击“在看”或分享至朋友圈让更多人看到哦~